package unimelb.filesync.common.protocol;

import java.io.IOException;
import java.util.concurrent.BlockingQueue;

import unimelb.filesync.common.message.FileSyncRequest;
import unimelb.filesync.common.message.NetworkMessage;
import unimelb.filesync.common.message.SequenceCompleteNotification;
import unimelb.filesync.common.misc.ProtocolContext;
import unimelb.filesync.common.network.NetworkConnection;
import unimelb.filesync.common.network.NetworkService;
import filesync.BlockUnavailableException;
import filesync.CopyBlockInstruction;
import filesync.EndUpdateInstruction;
import filesync.Instruction;
import filesync.NewBlockInstruction;
import filesync.StartUpdateInstruction;
import filesync.SynchronisedFile;

public class ProcessInstructionTask implements Runnable {

    private final SynchronisedFile syncFile;

    private final BlockingQueue<FileSyncRequest> readMsgQueue;

    private final ProtocolContext context;

    private long nextSequenceId;

    private long nextInstructionId;

    public ProcessInstructionTask(SynchronisedFile syncFile, ProtocolContext context) {
        super();
        this.context = context;
        this.syncFile = syncFile;
        this.readMsgQueue = context.getObject(ProtocolContext.READ_MSG_QUEUE);
        nextSequenceId = 0;
        nextInstructionId = 0;
    }

    @Override
    public void run() {
        while (true) {
            FileSyncRequest nextMsg = null;
            try {
                nextMsg = readMsgQueue.peek();
                if (nextMsg != null && isNextMsg(nextMsg)) {
                    readMsgQueue.take();
                    processInstruction(nextMsg.getInstruction());
                    incrementSeqAndInstrCounter(nextMsg.getInstruction());
                    if (nextMsg.getInstruction() instanceof EndUpdateInstruction) {
                        notifySequenceComplete(nextMsg.getSequenceId());
                    }
                } else {
                    Thread.sleep(500);
                }
            } catch (BlockUnavailableException e) {
                final Instruction newBlockInstr = requestNewBlockInstruction(nextMsg);
                try {
                    processInstruction(newBlockInstr);
                } catch (BlockUnavailableException ex) {
                    // will not happen
                }
                incrementSeqAndInstrCounter(newBlockInstr);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    private Instruction requestNewBlockInstruction(FileSyncRequest request) {
        final NetworkConnection hotLine = context.getObject(ProtocolContext.HOT_LINE_CONNECTION);
        final NetworkService networkService = new NetworkService();
        networkService.writeMessage(NetworkMessage.createNewBlockRequestMsg(request), hotLine);
        final NetworkMessage newBlockMsg = networkService.readMessage(hotLine);
        if (newBlockMsg == null) {
            System.exit(0);
        }
        final FileSyncRequest newBlockRequest = newBlockMsg.getContent();
        return newBlockRequest.getInstruction();
    }

    private void incrementSeqAndInstrCounter(Instruction instruction) {
        if (instruction instanceof StartUpdateInstruction || instruction instanceof NewBlockInstruction || instruction instanceof CopyBlockInstruction) {
            nextInstructionId++;
        } else if (instruction instanceof EndUpdateInstruction) {
            nextSequenceId++;
            nextInstructionId = 0;
        } else {
            throw new RuntimeException("Unexpected instruction type");
        }
    }

    private void processInstruction(Instruction instruction) throws BlockUnavailableException {
        try {
            syncFile.ProcessInstruction(instruction);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private boolean isNextMsg(FileSyncRequest msg) {
        return nextSequenceId == msg.getSequenceId() && nextInstructionId == msg.getInstructionId();
    }

    private void notifySequenceComplete(long sequenceId) {
        final NetworkConnection hotLine = context.getObject(ProtocolContext.HOT_LINE_CONNECTION);
        final NetworkService networkService = new NetworkService();
        networkService.writeMessage(NetworkMessage.createSequenceCompleteMsg(new SequenceCompleteNotification(sequenceId)), hotLine);
    }

}
